/*----------------------------------------------------------------------
 *
 * tableam.c
 *		表访问方法例程，太大而无法成为内联函数。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/access/table/tableam.c
 *
 * NOTES
 *	  请注意，这里的大多数函数在 tableam.h 中有文档，而不是
 *	  这里。 这是因为 tableam.h 中有很多内联函数，
 *	  如果需要不断在文件之间切换，会更难理解。
 *
 *----------------------------------------------------------------------
 */
#include "postgres.h"

#include <math.h>

#include "access/syncscan.h"
#include "access/tableam.h"
#include "access/xact.h"
#include "optimizer/plancat.h"
#include "port/pg_bitutils.h"
#include "storage/bufmgr.h"
#include "storage/shmem.h"
#include "storage/smgr.h"

/*
 * 控制并行工作者在并行顺序扫描期间块分配行为的常量
 * 从技术上讲，这些值不需要是2的幂，但将它们作为2的幂使数学更优化
 * 并使得降低步长更加均匀。
 */

/* 我们尝试将并行顺序扫描分解为的I/O块数 */
#define PARALLEL_SEQSCAN_NCHUNKS			2048
/* 当剩下的块数只有这个数字时，减少分配的大小 */
#define PARALLEL_SEQSCAN_RAMPDOWN_CHUNKS	64
/* 将并行I/O块的大小限制为此块数 */
#define PARALLEL_SEQSCAN_MAX_CHUNK_SIZE		8192

/* GUC 变量 */
char	   *default_table_access_method = DEFAULT_TABLE_ACCESS_METHOD;
bool		synchronize_seqscans = true;


/* ----------------------------------------------------------------------------
 * 槽函数。
 * ----------------------------------------------------------------------------
 */

const TupleTableSlotOps *
table_slot_callbacks(Relation fc_relation)
{
	const TupleTableSlotOps *fc_tts_cb;

	if (fc_relation->rd_tableam)
		fc_tts_cb = fc_relation->rd_tableam->slot_callbacks(fc_relation);
	else if (fc_relation->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
	{
		/*
		 * 历史上，FDWs期望在槽中存储堆元组。继续
		 * 提供一个给它们，以使FDWs适应新
		 * 版本更加轻松。堆槽相对于虚拟槽的成本是相当小的。
		 */
		fc_tts_cb = &TTSOpsHeapTuple;
	}
	else
	{
		/*
		 * 这些需要被支持，因为代码的某些部分（如COPY）
		 * 也需要为此类关系创建槽。将堆槽是正确的事情的知识
		 * 在这里集中似乎更好。
		 */
		Assert(fc_relation->rd_rel->relkind == RELKIND_VIEW ||
			   fc_relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
		fc_tts_cb = &TTSOpsVirtual;
	}

	return fc_tts_cb;
}

TupleTableSlot * table_slot_create(Relation fc_relation, List **fc_reglist)
{
	const TupleTableSlotOps *fc_tts_cb;
	TupleTableSlot *fc_slot;

	fc_tts_cb = table_slot_callbacks(fc_relation);
	fc_slot = MakeSingleTupleTableSlot(RelationGetDescr(fc_relation), fc_tts_cb);

	if (fc_reglist)
		*fc_reglist = lappend(*fc_reglist, fc_slot);

	return fc_slot;
}


/* ----------------------------------------------------------------------------
 * 表扫描函数。
 * ----------------------------------------------------------------------------
 */

TableScanDesc table_beginscan_catalog(Relation fc_relation, int fc_nkeys, struct ScanKeyData *fc_key)
{
	uint32		fc_flags = SO_TYPE_SEQSCAN |
	SO_ALLOW_STRAT | SO_ALLOW_SYNC | SO_ALLOW_PAGEMODE | SO_TEMP_SNAPSHOT;
	Oid			fc_relid = RelationGetRelid(fc_relation);
	Snapshot	fc_snapshot = RegisterSnapshot(GetCatalogSnapshot(fc_relid));

	return fc_relation->rd_tableam->scan_begin(fc_relation, fc_snapshot, fc_nkeys, fc_key,
											NULL, fc_flags);
}

void table_scan_update_snapshot(TableScanDesc fc_scan, Snapshot fc_snapshot)
{
	Assert(IsMVCCSnapshot(fc_snapshot));

	RegisterSnapshot(fc_snapshot);
	fc_scan->rs_snapshot = fc_snapshot;
	fc_scan->rs_flags |= SO_TEMP_SNAPSHOT;
}


/* ----------------------------------------------------------------------------
 * 并行表扫描相关函数。
 * ----------------------------------------------------------------------------
 */

Size table_parallelscan_estimate(Relation fc_rel, Snapshot fc_snapshot)
{
	Size		fc_sz = 0;

	if (IsMVCCSnapshot(fc_snapshot))
		fc_sz = add_size(fc_sz, EstimateSnapshotSpace(fc_snapshot));
	else
		Assert(fc_snapshot == SnapshotAny);

	fc_sz = add_size(fc_sz, fc_rel->rd_tableam->parallelscan_estimate(fc_rel));

	return fc_sz;
}

void table_parallelscan_initialize(Relation fc_rel, ParallelTableScanDesc fc_pscan,
							  Snapshot fc_snapshot)
{
	Size		fc_snapshot_off = fc_rel->rd_tableam->parallelscan_initialize(fc_rel, fc_pscan);

	fc_pscan->phs_snapshot_off = fc_snapshot_off;

	if (IsMVCCSnapshot(fc_snapshot))
	{
		SerializeSnapshot(fc_snapshot, (char *) fc_pscan + fc_pscan->phs_snapshot_off);
		fc_pscan->phs_snapshot_any = false;
	}
	else
	{
		Assert(fc_snapshot == SnapshotAny);
		fc_pscan->phs_snapshot_any = true;
	}
}

TableScanDesc table_beginscan_parallel(Relation fc_relation, ParallelTableScanDesc fc_parallel_scan)
{
	Snapshot	fc_snapshot;
	uint32		fc_flags = SO_TYPE_SEQSCAN |
	SO_ALLOW_STRAT | SO_ALLOW_SYNC | SO_ALLOW_PAGEMODE;

	Assert(RelationGetRelid(fc_relation) == fc_parallel_scan->phs_relid);

	if (!fc_parallel_scan->phs_snapshot_any)
	{
		/* 快照已被序列化 -- 恢复它 */
		fc_snapshot = RestoreSnapshot((char *) fc_parallel_scan +
								   fc_parallel_scan->phs_snapshot_off);
		RegisterSnapshot(fc_snapshot);
		fc_flags |= SO_TEMP_SNAPSHOT;
	}
	else
	{
		/* 由调用者传递的SnapshotAny（未序列化） */
		fc_snapshot = SnapshotAny;
	}

	return fc_relation->rd_tableam->scan_begin(fc_relation, fc_snapshot, 0, NULL,
											fc_parallel_scan, fc_flags);
}


/* ----------------------------------------------------------------------------
 * 索引扫描相关函数。
 * ----------------------------------------------------------------------------
 */

/*
 * 要执行该检查，只需启动索引扫描，创建必要的
 * 槽，执行堆查找，然后再次关闭所有内容。这可以
 * 进行优化，但从性能的角度来看不太可能重要。如果
 * 经常存在与唯一索引键匹配的活动索引指针，
 * 此例程的CPU开销不太可能重要。
 *
 * 请注意，当我们返回true时，如果访问方法支持
 * 通过单个索引条目存储多个行版本（如
 * 堆的HOT），*tid可能会被修改。
 */
bool table_index_fetch_tuple_check(Relation fc_rel,
							  ItemPointer fc_tid,
							  Snapshot fc_snapshot,
							  bool *fc_all_dead)
{
	IndexFetchTableData *fc_scan;
	TupleTableSlot *fc_slot;
	bool		fc_call_again = false;
	bool		fc_found;

	fc_slot = table_slot_create(fc_rel, NULL);
	fc_scan = table_index_fetch_begin(fc_rel);
	fc_found = table_index_fetch_tuple(fc_scan, fc_tid, fc_snapshot, fc_slot, &fc_call_again,
									fc_all_dead);
	table_index_fetch_end(fc_scan);
	ExecDropSingleTupleTableSlot(fc_slot);

	return fc_found;
}


/* ------------------------------------------------------------------------
 * 对单个元组进行非修改操作的函数
 * ------------------------------------------------------------------------
 */

void table_tuple_get_latest_tid(TableScanDesc fc_scan, ItemPointer fc_tid)
{
	Relation	fc_rel = fc_scan->rs_rd;
	const TableAmRoutine *fc_tableam = fc_rel->rd_tableam;

	/*
	 * 我们不期望对表元组获取最新TID的直接调用，具有有效的
	 * CheckXidAlive 用于目录或常规表。请参阅详细评论
	 * 在xact.c中，声明了这些变量。
	 */
	if (unlikely(TransactionIdIsValid(CheckXidAlive) && !bsysscan))
		elog(ERROR, "unexpected table_tuple_get_latest_tid call during logical decoding");

	/*
	 * 由于这可以使用用户提供的TID进行调用，所以不应过于信任输入。
	 */
	if (!fc_tableam->tuple_tid_valid(fc_scan, fc_tid))
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
				 errmsg("tid (%u, %u) is not valid for relation \"%s\"",
						ItemPointerGetBlockNumberNoCheck(fc_tid),
						ItemPointerGetOffsetNumberNoCheck(fc_tid),
						RelationGetRelationName(fc_rel))));

	fc_tableam->tuple_get_latest_tid(fc_scan, fc_tid);
}


/* ----------------------------------------------------------------------------
 * 用于简化修改的函数。
 * ----------------------------------------------------------------------------
 */

/*
 * simple_table_tuple_insert - 插入一个元组
 *
 * 目前，这个例程与 table_tuple_insert 的不同之处仅在于提供一个
 * 默认的命令 ID，并且不允许访问加速选项。
 */
void simple_table_tuple_insert(Relation fc_rel, TupleTableSlot *fc_slot)
{
	table_tuple_insert(fc_rel, fc_slot, GetCurrentCommandId(true), 0, NULL);
}

/*
 * simple_table_tuple_delete - 删除一个元组
 *
 * 当不期望对目标元组进行并发更新时（例如，因为我们在与元组
 * 关联的关系上有一个锁），可以使用此例程删除一个元组。任何故障
 * 都通过 ereport() 报告。
 */
void simple_table_tuple_delete(Relation fc_rel, ItemPointer fc_tid, Snapshot fc_snapshot)
{
	TM_Result	fc_result;
	TM_FailureData fc_tmfd;

	fc_result = table_tuple_delete(fc_rel, fc_tid,
								GetCurrentCommandId(true),
								fc_snapshot, InvalidSnapshot,
								true /* 等待提交 */ ,
								&fc_tmfd, false /* changingPart */ );

	switch (fc_result)
	{
		case TM_SelfModified:
			/* 元组在当前命令中已经被更新？ */
			elog(ERROR, "tuple already updated by self");
			break;

		case TM_Ok:
			/* 成功完成 */
			break;

		case TM_Updated:
			elog(ERROR, "tuple concurrently updated");
			break;

		case TM_Deleted:
			elog(ERROR, "tuple concurrently deleted");
			break;

		default:
			elog(ERROR, "unrecognized table_tuple_delete status: %u", fc_result);
			break;
	}
}

/*
 * simple_table_tuple_update - 替换一个元组
 *
 * 当不期望对目标元组进行并发更新时（例如，因为我们在与元组
 * 关联的关系上有一个锁），可以使用此例程更新一个元组。任何故障
 * 都通过 ereport() 报告。
 */
void simple_table_tuple_update(Relation fc_rel, ItemPointer fc_otid,
						  TupleTableSlot *fc_slot,
						  Snapshot fc_snapshot,
						  bool *fc_update_indexes)
{
	TM_Result	fc_result;
	TM_FailureData fc_tmfd;
	LockTupleMode fc_lockmode;

	fc_result = table_tuple_update(fc_rel, fc_otid, fc_slot,
								GetCurrentCommandId(true),
								fc_snapshot, InvalidSnapshot,
								true /* 等待提交 */ ,
								&fc_tmfd, &fc_lockmode, fc_update_indexes);

	switch (fc_result)
	{
		case TM_SelfModified:
			/* 元组在当前命令中已经被更新？ */
			elog(ERROR, "tuple already updated by self");
			break;

		case TM_Ok:
			/* 成功完成 */
			break;

		case TM_Updated:
			elog(ERROR, "tuple concurrently updated");
			break;

		case TM_Deleted:
			elog(ERROR, "tuple concurrently deleted");
			break;

		default:
			elog(ERROR, "unrecognized table_tuple_update status: %u", fc_result);
			break;
	}
}


/* ----------------------------------------------------------------------------
 * 实现块导向 AM 的并行扫描的辅助函数。
 * ----------------------------------------------------------------------------
 */

Size table_block_parallelscan_estimate(Relation fc_rel)
{
	return sizeof(ParallelBlockTableScanDescData);
}

Size table_block_parallelscan_initialize(Relation fc_rel, ParallelTableScanDesc fc_pscan)
{
	ParallelBlockTableScanDesc fc_bpscan = (ParallelBlockTableScanDesc) fc_pscan;

	fc_bpscan->base.phs_relid = RelationGetRelid(fc_rel);
	fc_bpscan->phs_nblocks = RelationGetNumberOfBlocks(fc_rel);
	/* 将 phs_syncscan 初始化与 initscan 中的类似逻辑进行比较 */
	fc_bpscan->base.phs_syncscan = synchronize_seqscans &&
		!RelationUsesLocalBuffers(fc_rel) &&
		fc_bpscan->phs_nblocks > NBuffers / 4;
	SpinLockInit(&fc_bpscan->phs_mutex);
	fc_bpscan->phs_startblock = InvalidBlockNumber;
	pg_atomic_init_u64(&fc_bpscan->phs_nallocated, 0);

	return sizeof(ParallelBlockTableScanDescData);
}

void table_block_parallelscan_reinitialize(Relation fc_rel, ParallelTableScanDesc fc_pscan)
{
	ParallelBlockTableScanDesc fc_bpscan = (ParallelBlockTableScanDesc) fc_pscan;

	pg_atomic_write_u64(&fc_bpscan->phs_nallocated, 0);
}

/*
 * 查找并设置扫描的起始块
 *
 * 决定并行顺序扫描应该从哪里开始。此函数可能被调用多次，
 * 每个并行工作者调用一次。我们必须小心，只设置一次起始块。
 */
void table_block_parallelscan_startblock_init(Relation fc_rel,
										 ParallelBlockTableScanWorker fc_pbscanwork,
										 ParallelBlockTableScanDesc fc_pbscan)
{
	BlockNumber fc_sync_startpage = InvalidBlockNumber;

	/* 重置我们用于控制分配大小的状态。 */
	memset(fc_pbscanwork, 0, sizeof(*fc_pbscanwork));

	StaticAssertStmt(MaxBlockNumber <= 0xFFFFFFFE,
					 "pg_nextpower2_32 may be too small for non-standard BlockNumber width");

	/*
	 * 我们根据关系的大小确定块大小。首先，我们将关系分割
	 * 成 PARALLEL_SEQSCAN_NCHUNKS 个块，但随后根据块大小
	 * 取下一个更高的 2 的幂次。这意味着我们将关系分割成介于
	 * PARALLEL_SEQSCAN_NCHUNKS 和 PARALLEL_SEQSCAN_NCHUNKS / 2 之间的
	 * 块。
	 */
	fc_pbscanwork->phsw_chunk_size = pg_nextpower2_32(Max(fc_pbscan->phs_nblocks /
													   PARALLEL_SEQSCAN_NCHUNKS, 1));

	/*
	 * 确保我们在处理较大表时不会超过最大块大小。这意味着对于
	 * 较大的表，我们可能会得到远远超过 PARALLEL_SEQSCAN_NCHUNKS 的
	 * 块。过大的块大小已被证明对同步扫描性能有害。
	 */
	fc_pbscanwork->phsw_chunk_size = Min(fc_pbscanwork->phsw_chunk_size,
									  PARALLEL_SEQSCAN_MAX_CHUNK_SIZE);

retry:
	/* 获取自旋锁。 */
	SpinLockAcquire(&fc_pbscan->phs_mutex);

	/*
	 * 如果扫描的起始块尚未初始化，我们必须现在进行初始化。
	 * 如果这不是一个同步扫描，我们只需从块 0 开始，但
	 * 如果这是一个同步扫描，我们必须从
	 * 同步扫描机制获取起始位置。不过，在做这个时，我们不能持有自旋锁，
	 * 所以释放自旋锁，获取我们需要的信息，然后重试。
	 * 如果在此期间没有其他人初始化扫描，我们将在第二次
	 * 经过时填写我们获取的值。
	 */
	if (fc_pbscan->phs_startblock == InvalidBlockNumber)
	{
		if (!fc_pbscan->base.phs_syncscan)
			fc_pbscan->phs_startblock = 0;
		else if (fc_sync_startpage != InvalidBlockNumber)
			fc_pbscan->phs_startblock = fc_sync_startpage;
		else
		{
			SpinLockRelease(&fc_pbscan->phs_mutex);
			fc_sync_startpage = ss_get_location(fc_rel, fc_pbscan->phs_nblocks);
			goto retry;
		}
	}
	SpinLockRelease(&fc_pbscan->phs_mutex);
}

/*
 * 获取下一个要扫描的页面
 *
 * 获取下一个要扫描的页面。即使没有页面可以扫描，
 * 另一个后端可能已经抓取了一个页面进行扫描但尚未完成
 * 查看，因此当第一个后端获得 InvalidBlockNumber 返回时，并不能说明扫描完成。
 */
BlockNumber table_block_parallelscan_nextpage(Relation fc_rel,
								  ParallelBlockTableScanWorker fc_pbscanwork,
								  ParallelBlockTableScanDesc fc_pbscan)
{
	BlockNumber fc_page;
	uint64		fc_nallocated;

	/*
	 * 下面的逻辑将块编号分配给并行工作线程，使得每个工作线程将接收到一组连续的块编号进行扫描。早期的版本会将下一个最高的块编号分配给下一个调用此函数的工作线程。这通常会导致工作线程接收到的块编号不连续。一些操作系统由于每个后端是不同的进程，无法检测到顺序I/O模式，这可能导致由于无效或没有预读而造成性能不佳。为了解决这个问题，我们现在为每个工作线程分配一个块编号范围，当他们返回请求下一个块时，我们将给予他们该范围内的下一个块，直到该范围的所有块都被分配完。当工作线程完成块范围后，我们会为其分配另一个范围，并返回该范围的第一个块编号。
	 *
	 * 在这里，我们将这些块的范围称为“块块”。这些块的初始大小在table_block_parallelscan_startblock_init中根据关系的大小来确定。在扫描快结束时，我们开始减小块的大小，以尽量将剩余的工作均匀分配给所有工作线程。
	 *
	 * 在这里，pbscanwork是本地工作线程内存。phsw_chunk_remaining跟踪块块中剩余的块数量。当这个数量达到0时，我们必须为工作线程分配一个新块块。
	 *
	 * phs_nallocated跟踪已经分配给工作线程的块数量。当phs_nallocated >= rs_nblocks时，所有块都已被分配。
	 *
	 * 由于我们使用原子取值并添加来获取当前值，phs_nallocated计数器会超过rs_nblocks，因为当工作线程尝试分配下一个块但所有块都已经被分配时，它们仍会增加该值。为了避免在rs_nblocks接近2^32时发生溢出，计数器必须是64位的。
	 *
	 * 实际返回的块是通过将计数器加到起始块编号并取模nblocks来计算的。
	 */

	/*
	 * 首先检查这个工作线程在之前的块块中是否还有剩余块。我们必须消耗完该块块中的所有块，才能为工作线程分配一个新的块块。
	 */
	if (fc_pbscanwork->phsw_chunk_remaining > 0)
	{
		/*
		 * 将范围内的下一个块给予他们，并更新剩余块的数量。
		 */
		fc_nallocated = ++fc_pbscanwork->phsw_nallocated;
		fc_pbscanwork->phsw_chunk_remaining--;
	}
	else
	{
		/*
		 * 当我们在扫描中仅剩下PARALLEL_SEQSCAN_RAMPDOWN_CHUNKS块块时，我们将块大小减半。由于我们在这里减少了块的大小，因此在对新大小进行PARALLEL_SEQSCAN_RAMPDOWN_CHUNKS操作后，我们会再次遇到这个情况。经过几次迭代后，我们最终会将最后几个块的块大小设置为1。
		 */
		if (fc_pbscanwork->phsw_chunk_size > 1 &&
			fc_pbscanwork->phsw_nallocated > fc_pbscan->phs_nblocks -
			(fc_pbscanwork->phsw_chunk_size * PARALLEL_SEQSCAN_RAMPDOWN_CHUNKS))
			fc_pbscanwork->phsw_chunk_size >>= 1;

		fc_nallocated = fc_pbscanwork->phsw_nallocated =
			pg_atomic_fetch_add_u64(&fc_pbscan->phs_nallocated,
									fc_pbscanwork->phsw_chunk_size);

		/*
		 * 设置此块中剩余的块数，以便来自该工作者的后续调用
		 * 将继续使用此块直到完成。
		 */
		fc_pbscanwork->phsw_chunk_remaining = fc_pbscanwork->phsw_chunk_size - 1;
	}

	if (fc_nallocated >= fc_pbscan->phs_nblocks)
		fc_page = InvalidBlockNumber;	/* 所有块都已分配 */
	else
		fc_page = (fc_nallocated + fc_pbscan->phs_startblock) % fc_pbscan->phs_nblocks;

	/*
	 * 报告扫描位置。 通常，我们报告当前页码。
	 * 然而，当我们到达扫描的结尾时，我们报告起始页，
	 * 而不是结束页，以便后续扫描的起始位置
	 * 不会向后偏移。 但我们只在扫描结束时报告一次位置：
	 * 后续调用者将不会报告任何内容。
	 */
	if (fc_pbscan->base.phs_syncscan)
	{
		if (fc_page != InvalidBlockNumber)
			ss_report_location(fc_rel, fc_page);
		else if (fc_nallocated == fc_pbscan->phs_nblocks)
			ss_report_location(fc_rel, fc_pbscan->phs_startblock);
	}

	return fc_page;
}

/* ----------------------------------------------------------------------------
 * 辅助函数，用于实现基于块的 AM 的关系大小。
 * ----------------------------------------------------------------------------
 */

/*
 * table_block_relation_size
 *
 * 如果一个表 AM 使用各种关系分支作为数据
 * 存储的唯一位置，并且以预期方式使用它们（例如，实际数据
 * 存在主分支而不是其他分支中），它可以使用此实现
 * 的 relation_size 回调，而不是实现自己的。
 */
uint64 table_block_relation_size(Relation fc_rel, ForkNumber fc_forkNumber)
{
	uint64		fc_nblocks = 0;

	/* InvalidForkNumber 表示返回所有分支的大小 */
	if (fc_forkNumber == InvalidForkNumber)
	{
		for (int fc_i = 0; fc_i < MAX_FORKNUM; fc_i++)
			fc_nblocks += smgrnblocks(RelationGetSmgr(fc_rel), fc_i);
	}
	else
		fc_nblocks = smgrnblocks(RelationGetSmgr(fc_rel), fc_forkNumber);

	return fc_nblocks * BLCKSZ;
}

/*
 * table_block_relation_estimate_size
 *
 * 该函数不能直接用作
 * relation_estimate_size 回调的实现，因为它有几个额外的参数。
 * 相反，它旨在作为辅助函数使用；调用者可以
 * 将参数传递给其 relation_estimate_size 函数加上
 * 此处所需的额外值。
 *
 * overhead_bytes_per_tuple 应该包含存储每个元组所需的
 * 近似字节数，超出元组数据本身所需的部分。通常，这将包括
 * 元组头和项目指针的大小。此信息仅用于查询
 * 规划，因此对于一个值不常数的表 AM 来说，可以选择
 * 传递一个“最佳猜测”。
 *
 * usable_bytes_per_page 应该包含每页用于元组数据的
 * 近似字节数，不包括页头和任何预期的特殊空间。
 */
void table_block_relation_estimate_size(Relation fc_rel, int32 *fc_attr_widths,
								   BlockNumber *fc_pages, double *fc_tuples,
								   double *fc_allvisfrac,
								   Size fc_overhead_bytes_per_tuple,
								   Size fc_usable_bytes_per_page)
{
	BlockNumber fc_curpages;
	BlockNumber fc_relpages;
	double		fc_reltuples;
	BlockNumber fc_relallvisible;
	double		fc_density;

	/* 它应该有存储，因此我们可以调用 smgr */
	fc_curpages = RelationGetNumberOfBlocks(fc_rel);

	/* 将 pg_class 中的值强制转换为更理想的类型 */
	fc_relpages = (BlockNumber) fc_rel->rd_rel->relpages;
	fc_reltuples = (double) fc_rel->rd_rel->reltuples;
	fc_relallvisible = (BlockNumber) fc_rel->rd_rel->relallvisible;

	/*
	 * HACK：如果关系从未进行过清理，则使用最小大小
	 * 估计为10页。这样做的目的是避免假设一个新创建的表确实很小，
	 * 即使它当前确实很小，因为一旦加载了一些数据，这可能不再成立。
	 * 一旦对其进行了清理或分析周期，才更合理地相信其大小相对稳定。
	 *
	 * （请注意，只有在计划被缓存并在表填充后再次使用时，
	 * 这才是一个问题。我们要避免的是在一个自上次计划制定以来
	 * 大幅增长的表上使用嵌套循环类型的计划。通常，自动清理/自动分析
	 * 会在足够多的插入发生后进行，并导致缓存计划失效；
	 * 但这并不是瞬时发生的，并且在临时表等情况下根本不会发生。）
	 *
	 * 我们通过检查 reltuples < 0 来测试“从未清理”。
	 *
	 * 如果表有继承子表，我们不应用此启发式。
	 * 完全为空的父表是相当常见的，因此我们应该愿意相信它们是空的。
	 */
	if (fc_curpages < 10 &&
		fc_reltuples < 0 &&
		!fc_rel->rd_rel->relhassubclass)
		fc_curpages = 10;

	/* 报告估计的页数 */
	*fc_pages = fc_curpages;
	/* 如果关系明显为空，则快速退出 */
	if (fc_curpages == 0)
	{
		*fc_tuples = 0;
		*fc_allvisfrac = 0;
		return;
	}

	/* 从先前的元组密度估计元组数 */
	if (fc_reltuples >= 0 && fc_relpages > 0)
		fc_density = fc_reltuples / (double) fc_relpages;
	else
	{
		/*
		 * 当我们没有数据因为关系从未进行清理时，
		 * 通过属性数据类型估计元组宽度。我们这里假设
		 * 页面是完全满的，这对表是可以的，但对索引来说
		 * 可能是高估。幸运的是
		 * get_relation_info() 可以将高估限制为父表的大小。
		 *
		 * 注意：这里的整数除法是故意的。
		 *
		 * 我们像使用页面和元组计数时那样缩放 relallvisible，
		 * 理论上讲，自上次清理以来添加的任何页面最有可能未标记为全可见。
		 * 但 costsize.c 希望它转为一个分数。
		 */
		int32		fc_tuple_width;

		fc_tuple_width = get_rel_data_width(fc_rel, fc_attr_widths);
		fc_tuple_width += fc_overhead_bytes_per_tuple;
		/* note: integer division is intentional here */
		fc_density = fc_usable_bytes_per_page / fc_tuple_width;
	}
	*fc_tuples = rint(fc_density * (double) fc_curpages);

	/*
	 * We use relallvisible as-is, rather than scaling it up like we do for
	 * the pages and tuples counts, on the theory that any pages added since
	 * the last VACUUM are most likely not marked all-visible.  But costsize.c
	 * wants it converted to a fraction.
	 */
	if (fc_relallvisible == 0 || fc_curpages <= 0)
		*fc_allvisfrac = 0;
	else if ((double) fc_relallvisible >= fc_curpages)
		*fc_allvisfrac = 1;
	else
		*fc_allvisfrac = (double) fc_relallvisible / fc_curpages;
}
